iT邦幫忙

2022 iThome 鐵人賽

DAY 21
1

Tracing

先前我們介紹了 Logging 與 Metrics 兩個服務監控機制,今天要來探討 Moleculer 內建的 Tracing 模組,它用於收集 Moleculer 內部應用的追蹤資訊,你可以使用 Tracing 工具來建立服務間的相互關係圖[Fig. 1.] 。內建有 ZipkinJaegerDatadog 可以直接使用。

注意,此功能可能會在 v0.15 版本進行大幅改動,你可能需要考慮是否要使用目前內建的模組,詳情請參考上一篇文章。


Fig. 1. Grafana Tempo 的 Node Graph[2]

範例:快速使用

moleculer.config.js

// moleculer.config.js
module.exports = {
    tracing: true
};

範例:選項設定方式

名稱 類型 預設值 說明
enabled <Boolean> false 啟動 tracing 功能
exporter <Object> | <Object[]> null Tracing 輸出配置
sampling <Object> 取樣設定
actions <Boolean> true 在 Actions 啟用 Tracing
events <Boolean> false 在事件啟用 Tracing
errorFields <String[]> ["name", "message", "code", "type", "data"] 要加到 Span 標籤的錯誤物件欄位
stackTrace <Boolean> false 是否要將錯誤案件的堆疊追蹤資訊加到 Span 標籤
tags <Object> null 加入客製化 Span 標籤到 Actions 與事件
defaultTags <Object> null 預設標籤,它會被加進所有的 spans 。

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        exporter: "Console",
        events: true,
        stackTrace: true
    }
};

取樣

Moleculer 有多種 Tracing 的取樣方法。它會由 Root Span 開始確認是否有正常取樣且傳播到所有的 Child Spans 。如此一來,可以確保無論使用哪種取樣方法或速率,都會輸出完整追蹤路線。

固定取樣

此取樣方法使用 0 ~ 1 的固定取樣率,設為 1 表示所有的 Spans 都將被取樣,設為 0 時則都不取樣。

範例:取樣所有 Spans

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            rate: 1.0
        }
    }
};

範例:取一半的 Spans

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            rate: 0.5
        }
    }
};

限制速率取樣

此取樣方法採用速率限制,你可以設置一秒鐘內可以取樣幾個 Spans 。

範例:每秒取樣 2 個 Spans

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            tracesPerSecond: 2
        }
    }
};

範例:每 10 秒取樣 1 個 Span

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            tracesPerSecond: 0.1
        }
    }
};

Tracing 輸出

Tracing 支援多個輸出器、客製化 tracing spans 及整合儀表板套件 (例如: dd-trace[3] )。

主控台

Console 是一種除錯用輸出器,可以將完整的本地 trace 列印到主控台。但是只能本地使用,不能遠端追蹤遠端呼叫。

範例:

module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Console",
            options: {
                // 客製化 logger
                logger: null,
                // 啟用顏色
                colors: true,
                // 寬度
                width: 100,
                // 測量寬度
                gaugeWidth: 40
            }
        }
    }
};

Datadog

Datadog 輸出器會透過 dd-trace[3] 將 tracing 資料發送到 Datadog 伺服器。

使用前請安裝 dd-trace 套件 npm install dd-trace --save


Fig. 2. Datadog

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Datadog",
            options: {
                // Datadog 服務器 URL
                agentUrl: process.env.DD_AGENT_URL || "http://localhost:8126",
                // 環境變數
                env: process.env.DD_ENVIRONMENT || null,
                // 取樣優先度,更多請參閱[4]
                samplingPriority: "AUTO_KEEP",
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null,
                // 客製化 Datadog Tracer 選項,更多請參閱[5]
                tracerOptions: null,
            }
        }
    }
};

事件

moleculer.config.js

Event 輸出器會將 tracing 資料發送到 Moleculer 事件 ( $tracing.spans ) 。

module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Event",
            options: {
                // 事件名稱
                eventName: "$tracing.spans",
                // 當 span 開始時發送事件
                sendStartSpan: false,
                // 當 span 結束後發送事件
                sendFinishSpan: true,
                // 是否廣播
                broadcast: false,
                // 事件群組
                groups: null,
                // 發送時間間隔 (秒)
                interval: 5,
                // 發送前的客製化 Span 物件轉換器
                spanConverter: null,
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};

事件(舊版)

為了避免與舊版混淆,本文不特別解說舊版的事件輸出器,會來看本系列的人應該也不會是舊版使用者,欲知詳情可以查看官方手冊說明:

https://moleculer.services/docs/0.14/tracing.html#Event-legacy

Jaeger

Jaeger 輸出器會發送 tracing spans 資訊到 Jaeger 伺服器。

使用前請安裝 jaeger-client 套件 npm install jaeger-client --save

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Jaeger",
            options: {
                // HTTP 報表產生器端點
                endpoint: null,
                // UDP 發送器 host
                host: "127.0.0.1",
                // UDP 發送器連接埠
                port: 6832,
                // Jaeger 取樣配置
                sampler: {
                    // 取樣類型[6] 
                    type: "Const",
                    // 取樣配置
                    options: {}
                },
                // `Jaeger.Tracer` 的附加選項
                tracerOptions: {},
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};

Zipkin

Zipkin 輸出器會發送 tracing spans 資訊到 Zipkin 伺服器。

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Zipkin",
            options: {
                // Zipkin 伺服器的 Base URL
                baseURL: "http://localhost:9411",
                // 發送時間間隔 (秒)
                interval: 5,
                // 附加資料選項
                payloadOptions: {
                    // 設定啟用 `debug` 屬性
                    debug: false,
                    // 設定啟用 `shared` 屬性
                    shared: false
                },
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};

NewRelic

NewRelic 輸出器會以 Zipkin v2 格式發送 tracing spans 資訊到 NewRelic 伺服器。

moleculer.config.js

{
    tracing: {
        enabled: true,
        events: true,
        exporter: [
            {
                type: 'NewRelic',
                options: {
                // NewRelic 伺服器的 Base URL
                    baseURL: 'https://trace-api.newrelic.com',
                    // NewRelic Insert Key
                    insertKey: 'my-secret-key',
                    // 發送時間間隔 (秒)
                    interval: 5,
                    // 附加資料選項
                    payloadOptions: {
                        // 設定啟用 `debug` 屬性
                        debug: false,
                        // 設定啟用 `shared` 屬性
                        shared: false,
                    },
                    // 預設標籤,它會被加進所有的 spans
                    defaultTags: null,
                },
            },
        ],
    },    
}

客製化輸出器

你也可以建立客製化的輸出器,官方建議可以參考 Console Exporter[9] 的原始碼來修改,再實作 initstopspanStartedspanFinished 方法。

範例:建立客製化 Tracing

my-tracing-exporter.js

const TracerBase = require("moleculer").TracerExporters.Base;

class MyTracingExporters extends TracerBase {
    init() { /*...*/ }
    stop() { /*...*/ }
    spanStarted() { /*...*/ }
    spanFinished() { /*...*/ }
}

module.exports = MyTracingExporters;

範例:使用客製化 Tracing

moleculer.config.js

const MyTracingExporters = require("./my-tracing-exporter");

module.exports = {
    tracing: {
        enabled: true,
        exporter: [
            new MyTracingExporters(),
        ]
    }
};

多輸出器

你可以在 exporter 以陣列來定義多個輸出器。

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        exporter: [
            "Console",
            {
                type: "Zipkin",
                options: {
                    baseURL: "http://localhost:9411",
                }
            },
            {
                type: "Jaeger",
                options: {
                    host: "127.0.0.1",
                }
            }
        ]
    }
};

使用者定義的 tracing spans

你可以在 Actions 或事件處理器裡面使用 ctx.startSpanctx.finishSpan 方法來加入新的 spans 。

posts.service.js

module.exports = {
    name: "posts",
    actions: {
        async find(ctx) {
            const span1 = ctx.startSpan("get data from DB", {
                tags: {
                    ...ctx.params
                }
            }); 
            const data = await this.getDataFromDB(ctx.params);
            ctx.finishSpan(span1);

            const span2 = ctx.startSpan("populating");
            const res = await this.populate(data);
            ctx.finishSpan(span2);

            return res;
        }
    }
};

範例:當 Context 不可用時,你也可以使用 broker.tracer 來建立 Span 。

posts.service.js

module.exports = {
    name: "posts",
    started() {
        // 建立一個 span 來初始化資料庫
        const span = this.broker.tracer.startSpan("initializing db", {
            tags: {
                dbHost: this.settings.dbHost
            }
        });

        await this.db.connect(this.settings.dbHost);

        // 建立 sub-span 來建立資料表
        const span2 = span.startSpan("create tables");

        await this.createDatabaseTables();

        // 結束 sub-span
        span2.finish();

        // 結束 span
        span.finish();
    }
};

使用外部通訊模組連接 Spans

當你使用外部隊列通訊時也可以連接到 Spans (例如: moleculer-channels[10] )。你只需要將 parentIDrequestID 丟給處理程序,然後使用這些 ID 來啟動客製化 span。

範例:連接 Spans

module.exports = {
	name: "trace",
	actions: {
		async extractTraces(ctx) {
			// 從 Context 取得 `parentID` 及 `requestID`
			const { parentID, requestID: traceID } = ctx;

			// 將 `parentID` 及 `traceID` 作為參數發送到遠端隊列
			await this.broker.sendToChannel("trace.setSpanID", {
				// 發送 IDs 參數
				parentID,
				traceID,
			});
		},
	},

	channels: {
		"trace.setSpanID"(payload) {
			// 將參數帶進客製化 span
			const span = this.broker.tracer.startSpan("my.span", payload);

			// ... 撰寫邏輯

			span.finish(); // 結束客製化 span
		},
	},
};

客製化

客製化 Span 名稱

你可以客製化 trace span 的名稱。這種情況下,必須將 spanName 設為一個 String 或是 Function

範例:建立客製化名稱函數

posts.service.js

module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                spanName: ctx => `Get a post by ID: ${ctx.params.id}`
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};

從 Context 加入標籤

你可以設定要將那些 Context 的 paramsmeta 加進 span 標籤。

範例:預設只會加入 ctx.params 的所有屬性

posts.service.js

module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // 加入 `params` ,但不加入 `meta`
                tags: {
                    params: true,
                    meta: false,
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};

範例:客製化 params

posts.service.js

module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                tags: {
                    // 加入 `ctx.params.id`
                    params: ["id"],
                    // 加入 `ctx.meta.loggedIn.username`
                    meta: ["loggedIn.username"],
                    // 在 action 響應中加入標籤
                    response: ["id", "title"]
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};

範例:使用客製化函數來產生 span 標籤

posts.service.js

module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // 可由引數取得 Context 資料
                tags(ctx, response) {
                    return {
                        params: ctx.params,
                        meta: ctx.meta,
                        custom: {
                            a: 5
                        },
                        response
                    };
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};

注意,當你在 Action 中使用這個方法時,執行成功的話函數將會被呼叫兩次。第一次只會響應 ctx 資訊,第二次則會包含 ctxresponse 資訊。

全域的 Action 與事件標籤

你可以在配置的 tracing.tags 中設定全域的 Action 與事件的 span 標籤,除非你在服務的 Action 與事件內去覆蓋設定,否則它將會套用到所有的 Action 與事件。所有前述的客製化標籤類型都是有效的。雖然在服務中 Action 與事件的標籤都是優先套用的,但 paramsmetaresponse 標籤的定義卻不是,意思就是你仍然可以在每個服務中,去定義全域的 meta 標籤或本地 response 標籤。

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        tags: {
            action: {
                // 永不加入 params
                params: false,
                // 由 `ctx.meta` 新增 `loggedIn.username` 值
                meta: ["loggedIn.username"],
                // 總是加入 response
                response: true,
            },
            event(ctx) {
                return {
                    params: ctx.params,
                    meta: ctx.meta,
                    // 加入呼叫功能
                    caller: ctx.caller,
                    custom: {
                        a: 5
                    },
                };
            },
        }
    }
};

使用配置的 tags 屬性所定義的客製化標籤可以使用 ctx 引數,如果是在 action 使用還可以拿到 response 。若是在 defaultTags 選項中定義的標籤則必須是靜態的物件,或是寫成一個函數並在接收 tracer 實例後返回一個物件。它也可以透過 tracer 實例請求 broker 實例,但無法透過 ctx 請求。

範例:事件 tracing 只需在設置中加上 events: true

moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,
        events: true
    }
};

safetyTags 與最大呼叫堆疊錯誤

通常不建議在 ctx.paramsctx.meta 發送不可序列化的參數(例如: http request 、 socket 實例及串流實例等)。如果啟用了 tracing ,則 tracer 輸出器將試著遞迴展平參數(使用 flattenTags method[11] ),這將會導致最大呼叫堆疊錯誤。

為了避免發生問題,可以在輸出器設定使用 safetyTags ,設為 true 的時候,輸出器將不會進入循環展平,此選項可以在任何的內建輸出器使用。

注意,當你啟動這個功能,有可能導致系統的效率下降。

範例:全域使用 safetyTags

moleculer.config.js

{
    tracing: {
        exporter: [{
            type: "Zipkin",
            options: {
                safetyTags: true,
                baseURL: "http://127.0.0.1:9411"
            }
        }]
    }
}

為避免影響所有的 Action ,你可以只在 Action 啟用 safetyTags ,以避免其它 Action 受到影響。

broker.createService({
    name: "greeter",
    actions: {
        hello: {
            tracing: {
                safetyTags: true
            },
            handler(ctx) {
                return `Hello!`;
            }
        }
    }
});

參考文獻

[1] Tracing, https://moleculer.services/docs/0.14/tracing.html
[2] 淺談OpenTelemetry Specification - Trace, https://ithelp.ithome.com.tw/articles/10288979
[3] dd-trace, https://github.com/DataDog/dd-trace-js
[4] Sampling rules, https://docs.datadoghq.com/tracing/faq/trace_sampling_and_storage/?tab=java#sampling-rules
[5] Tracer settings, https://datadoghq.dev/dd-trace-js/#tracer-settings
[6] Client Sampling Configuration, https://www.jaegertracing.io/docs/1.14/sampling/#client-sampling-configuration
[7] Zipkin, https://zipkin.io/
[8] new relic, https://newrelic.com/
[9] Moleculer Console Exporter, https://github.com/moleculerjs/moleculer/blob/master/src/tracing/exporters/console.js
[10] moleculer-channels, https://github.com/moleculerjs/moleculer-channels
[11] flattenTags method, https://github.com/moleculerjs/moleculer/blob/c48d5a05a4f4a1656075faaabc64085ccccf7ef9/src/tracing/exporters/base.js#L87-L101

家家酒小劇場

  • Otter - 這篇真的是看得頭昏眼花QAQ
  • Boxy - Tracing 的確是有點難說明,建議可以先看一些 Tracing 的文章,了解觀念後再來看如何撰寫,例如這篇[2] 。

上一篇
Day 20 : Metrics
下一篇
Day 22 : Errors
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言